Explora los tipos de plantillas literales de TypeScript y construye un motor de validación en tiempo de ejecución para una robusta verificación de cadenas y seguridad de tipos. Aprende a prevenir errores validando cadenas en tiempo de ejecución.
Motor de Validación de Plantillas Literales de TypeScript: Verificación de Cadenas en Tiempo de Ejecución
Los tipos de plantillas literales de TypeScript ofrecen una potente manipulación de cadenas y seguridad de tipos en tiempo de compilación. Sin embargo, estas verificaciones se limitan al tiempo de compilación. Este artículo explora cómo construir un motor de validación en tiempo de ejecución para los tipos de plantillas literales de TypeScript, permitiendo una verificación de cadenas robusta y previniendo errores potenciales durante la ejecución del programa.
Introducción a los Tipos de Plantillas Literales de TypeScript
Los tipos de plantillas literales te permiten definir formas de cadena específicas basadas en valores literales, uniones e inferencia de tipos. Esto permite una comprobación de tipos precisa y autocompletado, lo que es especialmente útil al tratar con datos estructurados o lenguajes de dominio específico.
Por ejemplo, considera un tipo para representar códigos de moneda:
type CurrencyCode = "USD" | "EUR" | "GBP";
type FormattedCurrencyString = `${CurrencyCode}-${number}`;
const validCurrency: FormattedCurrencyString = "USD-100"; // Correcto
const invalidCurrency: FormattedCurrencyString = "CAD-50"; // Error de tipo en tiempo de compilación
Este ejemplo demuestra cómo TypeScript impone el tipo FormattedCurrencyString en tiempo de compilación. Sin embargo, si el código de moneda proviene de una fuente externa (p. ej., entrada de usuario, respuesta de API), necesitas validación en tiempo de ejecución para garantizar la seguridad de tipos.
La Necesidad de la Validación en Tiempo de Ejecución
Aunque TypeScript proporciona una excelente comprobación de tipos en tiempo de compilación, no puede garantizar la validez de los datos recibidos de fuentes externas en tiempo de ejecución. Confiar únicamente en los tipos en tiempo de compilación puede llevar a errores inesperados y vulnerabilidades.
Considera el siguiente escenario:
function processCurrency(currencyString: FormattedCurrencyString) {
// ... algo de lógica que asume que la cadena está formateada correctamente
}
const userInput = "CAD-50"; // Asumimos que esto proviene de la entrada del usuario
// Esto compilará, pero causará un error en tiempo de ejecución si la lógica dentro de
// `processCurrency` depende del formato.
processCurrency(userInput as FormattedCurrencyString);
En este caso, estamos haciendo un cast de userInput a FormattedCurrencyString, eludiendo las comprobaciones en tiempo de compilación de TypeScript. Si processCurrency depende de que la cadena esté formateada correctamente, encontrará un error en tiempo de ejecución.
La validación en tiempo de ejecución cierra esta brecha al verificar que los datos recibidos en tiempo de ejecución se ajustan a los tipos de TypeScript esperados.
Construyendo un Motor de Validación de Plantillas Literales
Podemos construir un motor de validación en tiempo de ejecución usando expresiones regulares y el sistema de tipos de TypeScript. El motor tomará un tipo de plantilla literal y una cadena como entrada y devolverá si la cadena coincide con el tipo.
Paso 1: Definiendo un Tipo para la Validación en Tiempo de Ejecución
Primero, necesitamos un tipo genérico que pueda representar el equivalente en tiempo de ejecución de un tipo de plantilla literal. Este tipo debería ser capaz de manejar diferentes tipos de plantillas literales, incluyendo literales, uniones y parámetros de tipo.
type TemplateLiteralToRegex =
T extends `${infer Start}${infer Middle}${infer End}`
? Start extends string
? Middle extends string
? End extends string
? TemplateLiteralToRegexStart & TemplateLiteralToRegexMiddle & TemplateLiteralToRegex
: never
: never
: never
: TemplateLiteralToRegexStart;
type TemplateLiteralToRegexStart = T extends `${infer Literal}` ? Literal : string;
type TemplateLiteralToRegexMiddle = T extends `${infer Literal}` ? Literal : string;
Esta definición de tipo recursiva descompone la plantilla literal en sus partes constituyentes y convierte cada parte en un patrón de expresión regular.
Paso 2: Implementando la Función de Validación
A continuación, implementamos la función de validación que toma el tipo de plantilla literal y la cadena a validar como entrada. Esta función utiliza la expresión regular generada por TemplateLiteralToRegex para probar la cadena.
function isValid(str: string, templateType: T): boolean {
const regexPattern = `^${convertTemplateLiteralToRegex(templateType)}$`;
const regex = new RegExp(regexPattern);
return regex.test(str);
}
function convertTemplateLiteralToRegex(templateType: T): string {
// Conversión básica para cadenas literales: amplía esto para escenarios más complejos
return templateType.replace(/[.*+?^${}()|[\]]/g, '\\$&'); // Escapar caracteres especiales de regex
}
Esta función escapa los caracteres especiales de las expresiones regulares y crea una expresión regular a partir del tipo de plantilla literal, luego prueba la cadena contra esa expresión regular.
Paso 3: Usando el Motor de Validación
Ahora, puedes usar la función isValid para validar cadenas contra tus tipos de plantillas literales en tiempo de ejecución.
type CurrencyCode = "USD" | "EUR" | "GBP";
type FormattedCurrencyString = `${CurrencyCode}-${number}`;
const userInput1 = "USD-100";
const userInput2 = "CAD-50";
console.log(`'${userInput1}' es válido: ${isValid(userInput1, "USD-100" )}`); // true
console.log(`'${userInput2}' es válido: ${isValid(userInput2, "USD-100")}`); // false
console.log(`'${userInput1}' es válido: ${isValid(userInput1, `USD-${100}`)}`); // true
console.log(`'${userInput2}' es válido: ${isValid(userInput2, `USD-${100}`)}`); // false
Este ejemplo demuestra cómo usar la función isValid para validar la entrada del usuario contra el tipo FormattedCurrencyString. La salida mostrará si las cadenas de entrada se consideran válidas o no, según la plantilla literal especificada.
Escenarios de Validación Avanzados
El motor de validación básico se puede extender para manejar escenarios más complejos, como uniones, tipos condicionales y tipos recursivos.
Manejando Uniones
Para manejar uniones, puedes modificar el tipo TemplateLiteralToRegex para generar una expresión regular que coincida con cualquiera de los miembros de la unión.
type CurrencyCode = "USD" | "EUR" | "GBP";
type FormattedCurrencyString = `${CurrencyCode}-${number}`;
function isValidCurrencyCode(str: string, templateType: T): boolean {
const currencyCodes: CurrencyCode[] = ["USD", "EUR", "GBP"];
return currencyCodes.includes(str as CurrencyCode);
}
function isValidUnionFormattedCurrencyString(str: string): boolean {
const parts = str.split('-');
if(parts.length !== 2) return false;
const [currencyCode, amount] = parts;
if (!isValidCurrencyCode(currencyCode, currencyCode)) return false;
if (isNaN(Number(amount))) return false;
return true;
}
console.log(`'USD-100' es una cadena formateada válida: ${isValidUnionFormattedCurrencyString('USD-100')}`);
console.log(`'CAD-50' es una cadena formateada válida: ${isValidUnionFormattedCurrencyString('CAD-50')}`);
Manejando Tipos Condicionales
Los tipos condicionales se pueden manejar evaluando la condición en tiempo de ejecución y generando diferentes expresiones regulares según el resultado.
type IsString = T extends string ? true : false;
// Este ejemplo requiere una lógica más avanzada y no es completamente implementable usando regex simples.
// Las guardas de tipo en tiempo de ejecución ofrecen una solución más robusta en este escenario específico.
// El siguiente código es ilustrativo y necesitaría adaptación para manejar tipos condicionales complejos.
function isString(value: any): value is string {
return typeof value === 'string';
}
function isValidConditionalType(value: any): boolean {
return isString(value);
}
console.log(`'hello' es una cadena: ${isValidConditionalType('hello')}`);
console.log(`123 es una cadena: ${isValidConditionalType(123)}`);
Manejando Tipos Recursivos
Los tipos recursivos se pueden manejar definiendo una función recursiva que genere el patrón de expresión regular. Sin embargo, ten cuidado de evitar la recursión infinita y los errores de desbordamiento de pila. Para una recursión profunda, los enfoques iterativos con límites apropiados son cruciales.
Alternativas a las Expresiones Regulares
Aunque las expresiones regulares son una herramienta poderosa para la validación de cadenas, pueden ser complejas y difíciles de mantener. Otros enfoques para la validación en tiempo de ejecución incluyen:
- Funciones de Validación Personalizadas: Escribe funciones personalizadas para validar tipos específicos según los requisitos de tu aplicación.
- Guardas de Tipo (Type Guards): Usa guardas de tipo para acotar el tipo de una variable en tiempo de ejecución.
- Librerías de Validación: Aprovecha librerías de validación existentes como Zod o Yup para simplificar el proceso de validación.
Zod, por ejemplo, proporciona una declaración basada en esquemas que se traduce en una validación en tiempo de ejecución:
import { z } from 'zod';
const CurrencyCodeSchema = z.enum(['USD', 'EUR', 'GBP']);
const FormattedCurrencyStringSchema = z.string().regex(new RegExp(`^${CurrencyCodeSchema.enum.USD}|${CurrencyCodeSchema.enum.EUR}|${CurrencyCodeSchema.enum.GBP}-[0-9]+$`));
try {
const validCurrency = FormattedCurrencyStringSchema.parse("USD-100");
console.log("Moneda Válida:", validCurrency);
} catch (error) {
console.error("Moneda Inválida:", error);
}
try {
const invalidCurrency = FormattedCurrencyStringSchema.parse("CAD-50");
console.log("Moneda Válida:", invalidCurrency); //Esto no se ejecutará si el `parse` falla.
} catch (error) {
console.error("Moneda Inválida:", error);
}
Mejores Prácticas para la Validación en Tiempo de Ejecución
Al implementar la validación en tiempo de ejecución, ten en cuenta las siguientes mejores prácticas:
- Valida en la Frontera: Valida los datos tan pronto como ingresan a tu sistema (p. ej., entrada de usuario, respuestas de API).
- Proporciona Mensajes de Error Claros: Genera mensajes de error informativos para ayudar a los usuarios a entender por qué su entrada es inválida.
- Usa una Estrategia de Validación Consistente: Adopta una estrategia de validación consistente en toda tu aplicación para garantizar la integridad de los datos.
- Prueba tu Lógica de Validación: Prueba exhaustivamente tu lógica de validación para asegurarte de que identifica correctamente los datos válidos e inválidos.
- Equilibra Rendimiento y Seguridad: Optimiza tu lógica de validación para el rendimiento mientras te aseguras de que previene eficazmente las vulnerabilidades de seguridad. Evita expresiones regulares demasiado complejas que puedan conducir a una denegación de servicio.
Consideraciones de Internacionalización
Al tratar con la validación de cadenas en un contexto global, debes considerar la internacionalización (i18n) y la localización (l10n). Diferentes locales pueden tener diferentes reglas para formatear cadenas, como fechas, números y valores de moneda.
Por ejemplo, el símbolo de la moneda para el Euro (€) puede aparecer antes o después del monto, dependiendo del local. Del mismo modo, el separador decimal puede ser un punto (.) o una coma (,).
Para manejar estas variaciones, puedes usar librerías de internacionalización como Intl, que proporciona APIs para formatear y analizar datos sensibles al local. Por ejemplo, podrías adaptar el ejemplo anterior para manejar diferentes formatos de moneda:
function isValidCurrencyString(currencyString: string, locale: string): boolean {
try {
const formatter = new Intl.NumberFormat(locale, { style: 'currency', currency: currencyString.substring(0,3) }); //Ejemplo muy básico
//Intenta analizar la moneda usando el formateador. Este ejemplo es intencionalmente muy simple.
return true;
} catch (error) {
return false;
}
}
console.log(`USD-100 es válido para en-US: ${isValidCurrencyString('USD-100', 'en-US')}`);
console.log(`EUR-100 es válido para fr-FR: ${isValidCurrencyString('EUR-100', 'fr-FR')}`);
Este fragmento de código proporciona un ejemplo fundamental. Una internacionalización adecuada requiere un manejo más exhaustivo, utilizando potencialmente librerías externas o APIs diseñadas específicamente para el formato y la validación de monedas en diferentes locales.
Conclusión
La validación en tiempo de ejecución es una parte esencial de la construcción de aplicaciones TypeScript robustas y fiables. Al combinar los tipos de plantillas literales de TypeScript con expresiones regulares o métodos de validación alternativos, puedes crear un potente motor para verificar la validez de las cadenas en tiempo de ejecución.
Este enfoque mejora la seguridad de tipos, previene errores inesperados y mejora la calidad general de tu código. A medida que construyas aplicaciones más complejas, considera incorporar la validación en tiempo de ejecución para asegurar que tus datos se ajusten a los tipos y formatos esperados.
Exploración Adicional
- Explora técnicas avanzadas de expresiones regulares para escenarios de validación más complejos.
- Investiga librerías de validación como Zod y Yup para la validación basada en esquemas.
- Considera usar técnicas de generación de código para generar automáticamente funciones de validación a partir de tipos de TypeScript.
- Estudia librerías y APIs de internacionalización para manejar datos sensibles al local.